///////////////////////////////////////////////////////////////////////////////
//
//  The MIT License
//
//  Copyright (c) 2008 - 2009. Dimarik
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//
///////////////////////////////////////////////////////////////////////////////

package ru.goodcore.logging
{

import flash.utils.getClassName;
import flash.utils.getQualifiedClassName;

import ru.goodcore.logging.errors.LoggerError;
import ru.goodcore.logging.errors.LoggerErrorCode;
import ru.goodcore.logging.targets.ILoggingTarget;

/**
 *  Provides pseudo-hierarchical logging capabilities with multiple format and
 *  output options.
 *  The log system consists of two major components, the logger and a target.
 *  You can use the logger to send information to a target.
 *  The target is responsible for formatting and general output of the log data.
 *  <p>
 *  Loggers are singleton instances created for a particular category of
 *  information.
 *  Typically, the category is the package name of the component
 *  that desires to log information.
 *  The category provides users the ability to specify what log information they
 *  are interested in.
 *  Multiple categories can be selected and combined with regular expressions.
 *  This allows for both broad and narrow logging information to be acquired.
 *  For example, you might be interested in all logging information under
 *  the "ru.goodcore.transport" and "ru.goodcore.service" packages and want the
 *  output from these packages to be formatted as XML.
 *  To get the all of the logging information under the "ru.goodcore.transport"
 *  category including sub-packages and components a wildcard expression is
 *  required, such as "ru.goodcore.transport.~~".
 *  See the code example below for more details.
 *  </p>
 *  <p>Targets provide the output mechanism of the data being logged.
 *  This mechanism typically includes formatting, transmission, or storage, but
 *  can be anything possible under the VM.
 *  There are two targets provided: <code>MiniDebugTarget</code> and
 *  <code>TraceTarget</code>.
 *  Each of these writers take the current log information and "sends" it
 *
 *   somewhere for display and/or storage.
 *  Targets also provide the specification of what log data to output.
 *  </p>
 *
 *  @example
 *  <pre>
 *  ...
 *  import ru.goodcore.logging.targets.*;
 *  import ru.goodcore.logging.*;
 *
 *  private function initLogging():void {
 *      // Create a target.
 *      var logTarget:TraceTarget = new TraceTarget();
 *
 *      // Log only messages for the classes in the ru.goodcore.transport.* and
 *      // ru.goodcore.service packages.
 *      logTarget.filters=["ru.goodcore.transport.*","ru.goodcore.service.*"];
 *
 *      // Log all log levels.
 *      logTarget.level = LogEventLevel.ALL;
 *
 *      // Add date, time, category, and log level to the output.
 *      logTarget.includeDate = true;
 *      logTarget.includeTime = true;
 *      logTarget.includeCategory = true;
 *      logTarget.includeLevel = true;
 *
 *      // Begin logging.
 *      Log.addTarget(logTarget);
 *  } 
 *  ...
 *  </pre>
 */

/** TODO  learn carefully http://ddima.livejournal.com/64901.html  */
public class Log
{
    include "../../../includes/core/Version.as";

	//-------------------------------------------------------------------------
	//
	//  Class variables
	//
	//-------------------------------------------------------------------------

	/**
	 *  @private
	 *  Sentinal value for the target log level to indicate no logging.
	 */
	private static var NONE:			int = int.MAX_VALUE;

	/**
	 *  @private
	 *  The most verbose supported log level among registered targets.
	 */
	private static var _targetLevel:	int = NONE;
	// Initialize target level to a value out of range.

	/**
	 *  @private
	 *  An associative Array of existing loggers keyed by category
	 */
	private static var _loggers:		Object;

	/**
	 *  @private
	 *  Array of targets that should be searched any time
	 *  a new logger is created.
	 */
	private static var _targets:		Array = [];

	//--------------------------------------------------------------------------
	//
	//  Class methods
	//
	//--------------------------------------------------------------------------

	/**
	 *  Indicates whether a fatal level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if a fatal level log event will be logged; otherwise false.
	 */
	public static function isFatal():Boolean {
		return _targetLevel <= LogEventLevel.FATAL;
	}

	/**
	 *  Indicates whether an error level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if an error level log event will be logged; otherwise false.
	 */
	public static function isError():Boolean {
		return _targetLevel <= LogEventLevel.ERROR;
	}

	/**
	 *  Indicates whether a warn level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if a warn level log event will be logged; otherwise false.
	 */
	public static function isWarn():Boolean {
		return _targetLevel <= LogEventLevel.WARN;
	}

	/**
	 *  Indicates whether an info level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if an info level log event will be logged; otherwise false.
	 */ 
	public static function isInfo():Boolean {
		return _targetLevel <= LogEventLevel.INFO;
	}
	
	/**
	 *  Indicates whether a debug level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if a debug level log event will be logged; otherwise false.
	 */
	public static function isDebug():Boolean {
		return _targetLevel <= LogEventLevel.DEBUG;
	}

	/**
	 *  Allows the specified target to begin receiving notification of log
	 *  events.
	 *
	 *  @param target The specific target that should capture log events.
	 */
	public static function addTarget(target:ILoggingTarget):void
	{
		if (!target) {
			throw new LoggerError(LoggerErrorCode.INVALID_TARGET);
			return;
		}

		var filters:	Array = target.contentFilters;
		var logger:		ILogger;

		// need to find what filters this target matches and set the specified
		// target as a listener for that logger.
		for (var i:String in _loggers) {
			if (categoryMatchInFilterList(i, filters))
		    	target.addLogger(_loggers[i] as ILogger);
		}

		// if we found a match all is good, otherwise we need to
		// put the target in a waiting queue in the event that a logger
		// is created that this target cares about.
		_targets.push(target);

		if (_targetLevel == NONE) {
			_targetLevel = target.level;
		} else if (target.level < _targetLevel) {
			_targetLevel = target.level;
		}

	}
		
	/**
	 *  Stops the specified target from receiving notification of log
	 *  events.
	 *
	 *  @param target The specific target that should capture log events.
	 */
	public static function removeTarget(target:ILoggingTarget):void {
		if (!target) {
			throw new LoggerError(LoggerErrorCode.INVALID_TARGET);
			return;
		}

		var filters:	Array = target.contentFilters;
		var logger:		ILogger;

		// Disconnect this target from any matching loggers.
		for (var i:String in _loggers) {
			if (categoryMatchInFilterList(i, filters))
				target.removeLogger(ILogger(_loggers[i]));
		}

		// Remove the target.
		for (var j:int = 0; j < _targets.length; j++) {
			if (target == _targets[j]) {
				_targets.splice(j, 1);
				j--;
			}
		}

		resetTargetLevel();
	}
	
	/**
	 *  Returns the logger associated with the specified category.
	 *  If the category given doesn't exist a new instance of a logger will be
	 *  returned and associated with that category.
	 *  Categories must be at least one character in length and may not contain
	 *  any blanks or any of the following characters:
	 *  []~$^&amp;\/(){}&lt;&gt;+=`!#%?,:;'"&#64;
	 *  This method will throw an <code>LoggerError</code> if the
	 *  category specified is malformed.
	 *
	 *  @param value The value is a category of the logger that should be returned.
	 *  It can be an object, class, Number or String.
	 *
	 *  @return An instance of a logger object for the specified name.
	 *  If the name doesn't exist, a new instance with the specified
	 *  name is returned.
	 */
	public static function getLogger(value:*):ILogger {
		var category:String;

		if (value is String) {
			category = value;
		} else if (value is Number) {
			category = value.toString();
		} else if (value is Class || value is Object) {
			category = getClassName(value);
		}

		checkCategory(category);

		if (!_loggers)
			_loggers = [];
		
		// get the logger for the specified category or create one if it
		// doesn't exist
		var result:ILogger = _loggers[category];

		if (!result) {
			result = new LogLogger(category);
			_loggers[category] = result;
		}
		
		// check to see if there are any targets waiting for this logger.
		var target:ILoggingTarget;
		for (var i:int = 0; i < _targets.length; i++) {
			target = ILoggingTarget(_targets[i]);
			if (categoryMatchInFilterList(category, target.contentFilters))
				target.addLogger(result);
		}

		return result;
	}

	/**
	 *  This method removes all of the current loggers from the cache.
	 *  Subsquent calls to the <code>getLogger()</code> method return new instances
	 *  of loggers rather than any previous instances with the same category.
	 *  This method is intended for use in debugging only.
	 */
	public static function flush():void {
		_loggers = {};
		_targets = [];
		_targetLevel = NONE;
	}

	/**
	 *  This method checks the specified string value for illegal characters.
	 *
	 *  @param value The String to check for illegal characters.
	 *            The following characters are not valid:
	 *                []~$^&amp;\/(){}&lt;&gt;+=`!#%?,:;'"&#64;
	 *  @return   <code>true</code> if there are any illegal characters found,
	 *            <code>false</code> otherwise
	 */
	public static function hasIllegalCharacters(value:String):Boolean {
		return value.search(/[\[\]\~\$\^\&\\(\)\{\}\+\?\/=`!@#%,:;'"<>\s]/) != -1;
	}

	//-------------------------------------------------------------------------
	//
	//  Private class methods
	//
	//-------------------------------------------------------------------------

	/**
	 *  This method checks that the specified category matches any of the filter
	 *  expressions provided in the <code>filters</code> Array.
	 *
	 *  @param category The category to match against
	 *  @param filters A list of Strings to check category against.
	 *  @return <code>true</code> if the specified category matches any of the
	 *			filter expressions found in the filters list, <code>false</code>
	 *			otherwise.
	 *  @private
	 */
	private static function categoryMatchInFilterList(category:String, filters:Array):Boolean {
		var result:		Boolean = false;
		var filter:		String;
		var index:		int = -1;

		for (var i:uint = 0; i < filters.length; i++) {
			filter = filters[i];
			// first check to see if we need to do a partial match
			// do we have an asterisk?
			index = filter.indexOf("*");

			if (index == 0)
				return true;

			index = index < 0 ? index = category.length : index -1;

			if (category.substring(0, index) == filter.substring(0, index))
				return true;
		}
		return false;
	}

	/**
	 *  This method will ensure that a valid category string has been specified.
	 *  If the category is not valid an <code>LoggerError</code> will
	 *  be thrown.
	 *  Categories can not contain any blanks or any of the following characters:
	 *    []`*~,!#$%^&amp;()]{}+=\|'";?&gt;&lt;./&#64; or be less than 1 character in length.
	 *  @private
	 */
	private static function checkCategory(category:String):void {
		var message:String;

		if (category == null)
		{
			throw new LoggerError(LoggerErrorCode.INVALID_CATEGORY, category);
		}
		else if (category.length == 0)
		{
			throw new LoggerError(LoggerErrorCode.INVALID_LEN);
		}

		if (hasIllegalCharacters(category) || category.indexOf("*") != -1)
		{
			throw new LoggerError(LoggerErrorCode.INVALID_CHARS);
		}
	}
	
	/**
	 *  @private
	 *  This method resets the Log's target level to the most verbose log level
	 *  for the currently registered targets.
	 */
	private static function resetTargetLevel():void {
		var minLevel:int = NONE;
		for (var i:int = 0; i < _targets.length; i++)
		{
			if (minLevel == NONE || _targets[i].level < minLevel)
				minLevel = _targets[i].level;
		}
		_targetLevel = minLevel;
	}

}
}
